(* Yoann Padioleau
 *
 * Copyright (C) 2019-2023 Semgrep Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * version 2.1 as published by the Free Software Foundation, with the
 * special exception on linking described in file LICENSE.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
 * LICENSE for more details.
 *)
open Fpath_.Operators
open Pfff_or_tree_sitter
module Res = Parsing_result2
module Flag = Flag_semgrep
module E = Core_error
module OutJ = Semgrep_output_v1_t
module Log = Log_parsing.Log

(*****************************************************************************)
(* Prelude *)
(*****************************************************************************)
(* Parsing a target, using menhir or tree-sitter parsers or both
 * depending on the language.
 *)

(*****************************************************************************)
(* Helpers *)
(*****************************************************************************)

let lang_to_python_parsing_mode = function
  | Lang.Python -> Parse_python.Python
  | Lang.Python2 -> Parse_python.Python2
  | Lang.Python3 -> Parse_python.Python3
  | s -> failwith (Common.spf "not a python language:%s" (Lang.to_string s))

(* used by Match_search_mode and Match_tainting_mode *)
let errors_from_skipped_tokens xs =
  match xs with
  | [] -> Core_error.ErrorSet.empty
  | x :: _ ->
      let e = exn_of_loc x in
      let err = E.exn_to_error ~file:x.Loc.pos.file e in
      let locs =
        xs |> List_.map Semgrep_output_utils.location_of_token_location
      in
      Core_error.ErrorSet.singleton { err with typ = OutJ.PartialParsing locs }

(*****************************************************************************)
(* Entry point *)
(*****************************************************************************)

let just_parse_with_lang lang file : Parsing_result2.t =
  begin
    match lang with
    | Lang.C when Sys_.Fpath.exists !Flag_parsing_cpp.macros_h ->
        Parse_cpp.init_defs !Flag_parsing_cpp.macros_h
    | _ -> ()
  end;

  match lang with
  (* Neither Menhir nor tree-sitter *)
  | Lang.Scala ->
      run file
        [ Pfff (throw_tokens Parse_scala.parse) ]
        Scala_to_generic.program
  | Lang.Yaml ->
      {
        ast = Yaml_to_generic.program file;
        errors = [];
        skipped_tokens = [];
        inserted_tokens = [];
        tolerated_errors = [];
        stat = Parsing_stat.default_stat !!file;
      }
  (* Menhir and Tree-sitter *)
  | Lang.C
  | Lang.Cpp ->
      run file [ TreeSitter Parse_cpp_tree_sitter.parse ] Cpp_to_generic.program
  | Lang.Go ->
      run file
        [
          TreeSitter Parse_go_tree_sitter.parse;
          Pfff (throw_tokens Parse_go.parse);
        ]
        Go_to_generic.program
  | Lang.Java ->
      run file
        [ TreeSitter Parse_java_tree_sitter.parse ]
        Java_to_generic.program
  | Lang.Js ->
      (* we start directly with tree-sitter here, because
       * the pfff parser is slow on minified files due to its (slow) error
       * recovery strategy.
       *)
      run file
        [
          TreeSitter (Parse_typescript_tree_sitter.parse ~dialect:`TSX);
          Pfff (throw_tokens Parse_js.parse);
        ]
        Js_to_generic.program
  | Lang.Json ->
      run file
        [
          Pfff
            (fun file ->
              (Parse_json.parse_program file, Parsing_stat.correct_stat file));
        ]
        Json_to_generic.program
  | Lang.Ocaml ->
      run file
        [
          TreeSitter Parse_ocaml_tree_sitter.parse;
          Pfff (throw_tokens Parse_ml.parse);
        ]
        Ocaml_to_generic.program
  | Lang.Php ->
      run file
        [
          Pfff
            (fun file ->
              (* TODO: at some point parser_php.mly should go directly
               * to ast_php.ml and we should get rid of cst_php.ml
               *)
              let cst, stat = throw_tokens Parse_php.parse file in
              (Ast_php_build.program cst, stat));
          (* TODO: can't put TreeSitter first, because we still use Pfff
           * to parse the pattern, and there must be mismatch between the
           * AST generated by Ast_php_build and Parse_php_tree_sitter.parse.
           *)
          TreeSitter Parse_php_tree_sitter.parse;
        ]
        Php_to_generic.program
  | Lang.Python
  | Lang.Python2
  | Lang.Python3 ->
      let parsing_mode = lang_to_python_parsing_mode lang in
      run file
        [
          Pfff (throw_tokens (Parse_python.parse ~parsing_mode));
          TreeSitter Parse_python_tree_sitter.parse;
        ]
        Python_to_generic.program
  (* Tree-sitter only *)
  | Lang.Bash ->
      run file
        [ TreeSitter Parse_bash_tree_sitter.parse ]
        Bash_to_generic.program
  | Lang.Dockerfile ->
      run file
        [ TreeSitter Parse_dockerfile_tree_sitter.parse ]
        Dockerfile_to_generic.program
  | Lang.Jsonnet ->
      run file
        [ TreeSitter Parse_jsonnet_tree_sitter.parse ]
        Jsonnet_to_generic.program
  | Lang.Ql ->
      run file [ TreeSitter Parse_ql_tree_sitter.parse ] QL_to_generic.program
  | Lang.Terraform ->
      run file
        [ TreeSitter Parse_terraform_tree_sitter.parse ]
        Terraform_to_generic.program
  | Lang.Ts ->
      run file
        [ TreeSitter (Parse_typescript_tree_sitter.parse ?dialect:None) ]
        Js_to_generic.program
  | Lang.Vue -> failwith "Vue support has been removed in 1.93.0"
  (* there is no pfff parsers for C#/Kotlin/... so let's just use
   * tree-sitter, and there's no ast_xxx.ml either so we directly generate
   * a generic AST (no calls to an xxx_to_generic() below)
   *)
  | Lang.Cairo ->
      run file [ TreeSitter Parse_cairo_tree_sitter.parse ] (fun x -> x)
  | Lang.Ruby ->
      run file
        [ TreeSitter Parse_ruby_tree_sitter.parse ]
        Ruby_to_generic.program
  (* tree-sitter-dart is currently buggy and can generate some segfaults *)
  | Lang.Dart ->
      run file [ TreeSitter Parse_dart_tree_sitter.parse ] (fun x -> x)
  | Lang.Hack ->
      run file [ TreeSitter Parse_hack_tree_sitter.parse ] (fun x -> x)
  | Lang.Html
  (* TODO: there is now https://github.com/ObserverOfTime/tree-sitter-xml
   * which we could use for XML instead of abusing tree-sitter-html
   *)
  | Lang.Xml ->
      (* less: there is an html parser in pfff too we could use as backup *)
      run file [ TreeSitter Parse_html_tree_sitter.parse ] (fun x -> x)
  | Lang.Julia ->
      run file [ TreeSitter Parse_julia_tree_sitter.parse ] (fun x -> x)
  | Lang.Kotlin ->
      run file [ TreeSitter Parse_kotlin_tree_sitter.parse ] (fun x -> x)
  | Lang.Lisp
  | Lang.Scheme
  | Lang.Clojure ->
      run file [ TreeSitter Parse_clojure_tree_sitter.parse ] (fun x -> x)
  | Lang.Lua -> run file [ TreeSitter Parse_lua_tree_sitter.parse ] (fun x -> x)
  | Lang.Promql ->
      run file [ TreeSitter Parse_promql_tree_sitter.parse ] (fun x -> x)
  | Lang.Protobuf ->
      run file [ TreeSitter Parse_protobuf_tree_sitter.parse ] (fun x -> x)
  | Lang.Rust ->
      run file [ TreeSitter Parse_rust_tree_sitter.parse ] (fun x -> x)
  | Lang.Solidity ->
      run file [ TreeSitter Parse_solidity_tree_sitter.parse ] (fun x -> x)
  | Lang.Swift ->
      run file [ TreeSitter Parse_swift_tree_sitter.parse ] (fun x -> x)
  | Lang.R -> run file [ TreeSitter Parse_r_tree_sitter.parse ] (fun x -> x)
  | Lang.Move_on_sui ->
      run file [ TreeSitter Parse_move_on_sui_tree_sitter.parse ] (fun x -> x)
  | Lang.Move_on_aptos ->
      run file [ TreeSitter Parse_move_on_aptos_tree_sitter.parse ] (fun x -> x)
  | Lang.Circom ->
      run file [ TreeSitter Parse_circom_tree_sitter.parse ] (fun x -> x)
  (* External proprietary parsers. The parsers need to register themselves
   * for parsing to take place.
   *)
  | Lang.Apex -> run_external_parser file Parsing_plugin.Apex.parse_target
  | Lang.Csharp ->
      let parse_target =
        (* Use the proprietary parser if available *)
        if Parsing_plugin.Csharp.is_available () then
          Parsing_plugin.Csharp.parse_target
        else Parse_csharp_tree_sitter.parse
      in
      run file [ TreeSitter parse_target ] (fun x -> x)
  | Lang.Elixir -> run_external_parser file Parsing_plugin.Elixir.parse_target
  | Lang.Gosu -> run_external_parser file Parsing_plugin.Gosu.parse_target

let run_analyses_after_name_resolution lang ast =
  Typing.check_program lang ast;

  Implicit_return.mark_implicit_return lang ast;

  (* Flow-insensitive constant propagation. *)
  Constant_propagation.propagate_basic lang ast;

  (* Flow-sensitive constant propagation. *)
  Constant_propagation.propagate_dataflow lang ast

let just_resolve_name lang ast =
  Naming_AST.resolve lang ast;
  run_analyses_after_name_resolution lang ast

let parse_and_resolve_name lang file =
  let res = just_parse_with_lang lang file in
  let ast = res.ast in
  just_resolve_name lang ast;
  Log.info (fun m -> m "Parse_target.parse_and_resolve_name done");
  res
[@@profiling]

(* used in test files *)
let parse_and_resolve_name_warn_if_partial lang file =
  let { ast; errors; _ } : Res.t = parse_and_resolve_name lang file in
  (match errors with
  | [] -> ()
  | _ ->
      (* nosemgrep: no-logs-in-library *)
      Logs.warn (fun m -> m "fail to fully parse %s" !!file));
  ast

let fail_on_errors errors =
  match errors with
  | [] -> ()
  | errors ->
      let error_strs =
        List_.map (fun (Tree_sitter_error err : Res.error) -> err.msg) errors
      in
      failwith (String.concat "\n" error_strs)

let parse_and_resolve_name_fail_if_partial lang file =
  let { ast; errors; _ } : Res.t = parse_and_resolve_name lang file in
  fail_on_errors errors;
  ast

let parse_and_resolve_name_strict lang file =
  let { ast; errors; tolerated_errors; _ } : Res.t =
    parse_and_resolve_name lang file
  in
  fail_on_errors (errors @ tolerated_errors);
  ast

(*****************************************************************************)
(* For testing purpose *)
(*****************************************************************************)
let parse_program file =
  let lang = Lang.lang_of_filename_exn file in
  let res = just_parse_with_lang lang file in
  res.ast
